package com.billdowney.service.impl.file;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;

import javax.activation.MimetypesFileTypeMap;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.billdowney.dao.file.FileInfoDao;
import com.billdowney.entity.file.FileInfo;
import com.billdowney.entity.user.UserLoginInfo;
import com.billdowney.exception.BizException;
import com.billdowney.service.impl.base.BaseServiceImpl;
import com.billdowney.service.interfaces.file.FileInfoService;
import com.billdowney.service.interfaces.system.SystemDataDictService;
import com.billdowney.service.util.system.SystemDataDictConst;
import com.billdowney.util.CommonUtil;
import com.billdowney.util.DateUtil;
import com.billdowney.util.file.FileUtil;

/**
 * 文件信息接口实现
 * 
 * @author 超级小富翁 - BillDowney
 * @data 2017年10月23日 下午5:51:20
 */
@Service
public class FileInfoServiceImpl extends BaseServiceImpl<FileInfoDao, FileInfo, String> implements FileInfoService {

	/**
	 * 一次读取文件的数据大小
	 */
	private static int DATA_SIZE = 10240;

	@Autowired
	private SystemDataDictService systemDataDictService;

	@Resource(type = FileInfoDao.class)
	@Override
	public void setDao(FileInfoDao dao) {
		super.dao = dao;
	}

	@Override
	public FileInfo saveFileInfo(FileInfo fileInfo) {
		return dao.save(fileInfo);
	}

	@Override
	public FileInfo findFileInfoById(String id) throws BizException {
		Optional<FileInfo> fileInfo = dao.findById(id);
		if (fileInfo.isPresent()) {
			fileInfo.get().setVisitNum(fileInfo.get().getVisitNum() + 1);
			return this.saveFileInfo(fileInfo.get());
		} else {
			throw new BizException(10105);
		}
	}

	@Override
	public boolean updateFileInfoDownloadNum(String id) {
		Optional<FileInfo> fileInfo = dao.findById(id);
		if (fileInfo.isPresent()) {
			fileInfo.get().setDownloadNum(fileInfo.get().getDownloadNum() + 1);
			this.saveFileInfo(fileInfo.get());
			return true;
		} else {
			return false;
		}
	}

	@Override
	public List<String> saveAllMultipartFile(Map<String, MultipartFile> files, boolean isPublic) throws Exception {
		List<String> ids = new ArrayList<>();
		if (CommonUtil.isEmpty(files)) {
			throw new BizException(10102);
		}
		Set<String> keys = files.keySet();
		for (String key : keys) {
			ids.add(this.saveMultipartFile(files.get(key), isPublic).getId());
		}
		return ids;
	}

	@Override
	public FileInfo saveMultipartFile(MultipartFile multipartFile, boolean isPublic) throws Exception {
		// 获取项目默认存储地址
		String defaultPath = systemDataDictService.getDefaultFilePath();
		FileInfo fileInfo = new FileInfo();
		if (!multipartFile.isEmpty()) {
			// 文件上传时的字段名
			fileInfo.setFieldName(multipartFile.getName());
			// 原文件名
			fileInfo.setFileName(multipartFile.getOriginalFilename());
			// 保存的文件名
			fileInfo.setSaveFileName(System.currentTimeMillis() + "_" + new Random().nextInt(100) + "_"
					+ multipartFile.getOriginalFilename());
			// 文件大小，单位：字节(B)，1KB=1024B
			fileInfo.setFileSize(multipartFile.getSize());
			// 文件类型
			fileInfo.setFileType(multipartFile.getContentType());
			// 服务器本地路径
			fileInfo.setLocalPath(defaultPath + DateUtil.getCurrentYear() + "/" + DateUtil.getCurrentMonth() + "/"
					+ DateUtil.getCurrentDay() + "/");
			// 服务器本地文件路径
			fileInfo.setLocalFilePath(fileInfo.getLocalPath() + fileInfo.getSaveFileName());
			// 设置文件后缀
			fileInfo.setFileSuffix(FileUtil.getFileSuffix(fileInfo.getFileName()));
			// 设置上传用户
			try {
				fileInfo.setUserInfo(super.getUserLoginInfo().getUserInfo());
			} catch (Exception e) {
				System.err.println("当前未登录用户，文件未关联用户！");
			}
			// 设置是否公用
			if (isPublic) {
				fileInfo.setIsPublic(1);
			}
			try {
				FileUtil.createDir(fileInfo.getLocalPath());
				multipartFile.transferTo(new File(fileInfo.getLocalPath(), fileInfo.getSaveFileName()));
				this.saveFileInfo(fileInfo);
			} catch (Exception e) {
				throw new BizException(10103, e);
			}
		} else {
			throw new BizException(10102);
		}
		return fileInfo;
	}

	@Override
	public void fileDownloadById(String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
		FileInfo fileInfo = this.findFileInfoById(id);
		// 验证文件是否可以被下载
		if (fileInfo.getIsPublic() == 0 && CommonUtil.isNotEmpty(fileInfo.getUserInfo())) {
			try {
				UserLoginInfo userLoginInfo = super.getUserLoginInfo();
				if (!fileInfo.getUserInfo().getId().equals(userLoginInfo.getId())) {
					throw new BizException(10106);
				}
			} catch (Exception e) {
				throw new BizException(10106);
			}
		}
		this.fileDownload(fileInfo, request, response);
		this.updateFileInfoDownloadNum(id);
	}

	/**
	 * @Description: 组装数据到输出流，分段输出
	 * @param istream
	 * @param ostream
	 * @param start
	 * @param end
	 * @throws IOException
	 * @author: 唐路钧
	 * @date: 2019年1月8日 下午3:15:31
	 */
	protected void copyRange(InputStream istream, OutputStream ostream, long start, long end) throws IOException {
		long skipped = 0;
		skipped = istream.skip(start);
		if (skipped < start) {
			throw new IOException("skip fail: skipped=" + Long.valueOf(skipped) + ", start=" + Long.valueOf(start));
		}
		long bytesToRead = end - start + 1;
		byte buffer[] = new byte[DATA_SIZE];
		int len = buffer.length;
		// 当前下载速度100kb/s
		int downloadFileSpeed = 0;
		try {
			downloadFileSpeed = Integer
					.parseInt(systemDataDictService.getString(SystemDataDictConst.downloadFileSpeed).toString()) * 1024;
		} catch (Exception e1) {
			e1.printStackTrace();
		}
		// 已经输出的bytes
		int outBytes = 0;
		while ((bytesToRead > 0) && (len >= buffer.length)) {
			try {
				len = istream.read(buffer);
				if (bytesToRead >= len) {
					ostream.write(buffer, 0, len);
					bytesToRead -= len;
					outBytes += len;
					if (downloadFileSpeed > 0 && outBytes >= downloadFileSpeed) {
						try {
							Thread.sleep(999);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						outBytes = 0;
					}
				} else {
					ostream.write(buffer, 0, (int) bytesToRead);
					bytesToRead = 0;
				}
			} catch (IOException e) {
				len = -1;
				throw e;
			}
			if (len < buffer.length)
				break;
		}
	}

	@Override
	public int delFileByIds(String[] ids) throws Exception {
		int nums = 0;
		// 获取登录的用户信息
		UserLoginInfo userLoginInfo = super.getUserLoginInfo();
		for (String id : ids) {
			FileInfo fileInfo = this.findFileInfoById(id);
			if (CommonUtil.isNotEmpty(fileInfo)) {
				if (!(CommonUtil.isNotEmpty(fileInfo.getUserInfo())
						&& fileInfo.getUserInfo().getId() == userLoginInfo.getId())) {
					try {
						FileUtil.deleteAllFiles(fileInfo.getLocalFilePath());
						dao.delete(fileInfo);
						nums++;
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
		return nums;
	}

	@Override
	public int modifyPublicState(String[] ids) throws Exception {
		int nums = 0;
		// 获取登录的用户信息
		UserLoginInfo userLoginInfo = super.getUserLoginInfo();
		for (String id : ids) {
			FileInfo fileInfo = this.findFileInfoById(id);
			if (CommonUtil.isNotEmpty(fileInfo)) {
				if (!(CommonUtil.isNotEmpty(fileInfo.getUserInfo())
						&& fileInfo.getUserInfo().getId() == userLoginInfo.getId())) {
					try {
						if (fileInfo.getIsPublic() == 0) {
							fileInfo.setIsPublic(1);
						} else {
							fileInfo.setIsPublic(0);
						}
						this.saveFileInfo(fileInfo);
						nums++;
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
		return nums;
	}

	@Override
	public FileInfo modifyFileName(String id, String fileName) throws Exception {
		if (CommonUtil.isEmpty(fileName)) {
			throw new BizException(10107);
		}
		try {
			FileInfo fileInfo = this.findFileInfoById(id);
			fileInfo.setFileName(fileName);
			return this.saveFileInfo(fileInfo);
		} catch (Exception e) {
			throw new BizException(10108);
		}
	}

	@Override
	public void fileDownload(FileInfo fileInfo, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		this.fileDownload(new File(fileInfo.getLocalFilePath()), fileInfo.getFileName(), request, response);
	}

	@Override
	public void fileDownload(File downloadFile, String fileName, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		if (CommonUtil.isEmpty(fileName)) {
			fileName = downloadFile.getName();
		}
		long fileLength = downloadFile.length();// 记录文件大小
		long pastLength = 0;// 记录已下载文件大小
		long toLength = 0;// 记录客户端需要下载的字节段的最后一个字节偏移量（比如bytes=27000-39000，则这个值是为39000）
		long contentLength = 0;// 客户端请求的字节总量
		String rangeBytes = "";// 记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容
		// ETag header
		// The ETag is contentLength + lastModified
		response.setHeader("ETag", "W/\"" + fileLength + "-" + downloadFile.lastModified() + "\"");
		// Last-Modified header
		response.setHeader("Last-Modified", new Date(downloadFile.lastModified()).toString());
		if (request.getHeader("Range") != null) {// 客户端请求的下载的文件块的开始字节
			response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
			rangeBytes = request.getHeader("Range").replaceAll("bytes=", "");
			if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {// bytes=969998336-
				rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
				pastLength = Long.parseLong(rangeBytes.trim());
				toLength = fileLength - 1;
			} else {// bytes=1275856879-1275877358
				String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-'));
				String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length());
				// bytes=1275856879-1275877358，从第 1275856879个字节开始下载
				pastLength = Long.parseLong(temp0.trim());
				// bytes=1275856879-1275877358，到第 1275877358 个字节结束
				toLength = Long.parseLong(temp2);
			}
		} else {// 从开始进行下载
			toLength = fileLength - 1;
		}
		// 客户端请求的是1275856879-1275877358 之间的字节
		contentLength = toLength - pastLength + 1;
		if (contentLength < Integer.MAX_VALUE) {
			response.setContentLength((int) contentLength);
		} else {
			// Set the content-length as String to be able to use a long
			response.setHeader("Content-Length", "" + contentLength);
		}
		String contentType = new MimetypesFileTypeMap().getContentType(fileName);
		if (null != contentType) {
			response.setContentType(contentType);
		}
		// 告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-Ranges: bytes
		response.setHeader("Accept-Ranges", "bytes");
		// 必须先设置content-length再设置header
		response.addHeader("Content-Range", "bytes " + pastLength + "-" + toLength + "/" + fileLength);
		response.setBufferSize(DATA_SIZE);
		// 解决中文文件名显示问题
		/*
		 * try { response.setHeader("Content-Disposition", "attachment;filename=" + new
		 * String(fileName.getBytes("gb2312"), "ISO-8859-1")); } catch
		 * (UnsupportedEncodingException e1) { response.setHeader("Content-Disposition",
		 * "attachment;fileName=" + fileName); e1.printStackTrace(); }
		 */
		// if (request.getHeader("User-Agent").toUpperCase().indexOf("MSIE") != -1
		if (request.getHeader("User-Agent").toUpperCase().indexOf("FIREFOX") != -1) {
			fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
		} else {
			fileName = URLEncoder.encode(fileName, "UTF-8");
		}
		response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
		InputStream istream = null;
		OutputStream os = null;
		try {
			os = response.getOutputStream();
			istream = new BufferedInputStream(new FileInputStream(downloadFile), DATA_SIZE);
			try {
				copyRange(istream, os, pastLength, toLength);
			} catch (IOException ie) {
				/**
				 * 在写数据的时候， 对于 ClientAbortException 之类的异常， 是因为客户端取消了下载，而服务器端继续向浏览器写入数据时，
				 * 抛出这个异常，这个是正常的。 尤其是对于迅雷这种吸血的客户端软件， 明明已经有一个线程在读取 bytes=1275856879-1275877358，
				 * 如果短时间内没有读取完毕，迅雷会再启第二个、第三个。。。线程来读取相同的字节段， 直到有一个线程读取完毕，迅雷会 KILL
				 * 掉其他正在下载同一字节段的线程， 强行中止字节读出，造成服务器抛 ClientAbortException。 所以，我们忽略这种异常
				 */
				// ignore
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (istream != null) {
				try {
					istream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

}
